package jm.easyconfig;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.DigestUtils;

public class ProfilesHelper {
	ConfigurableEnvironment environment;
	String[] profiles;

	final static String DEFAULT = "default";
	final static String PROFILES_ACTIVE_PROPERTY_NAME = "easyconfig.profiles.active";
	final static String SLOT_PROPERTY_SOURCE = "easyconfig_$profile$_property_source";

	public ProfilesHelper(ConfigurableEnvironment environment) {
		this.environment = environment;

		String proval = this.environment.getProperty(PROFILES_ACTIVE_PROPERTY_NAME);
		if (proval == null) {
			proval = this.environment.getProperty("spring.profiles.active");
		}
		if (proval == null) {
			proval = this.environment.getProperty(
					this.environment.getProperty("easyconfig.boot-profile-holder", "easyconfig.boot-profile"));
		}

		String[] profilesActive = proval == null ? new String[0] : proval.split(",");

		LinkedList<String> lst = new LinkedList<String>();
		lst.add(DEFAULT);

		for (String pro : profilesActive) {
			pro = pro.trim();

			if (Objects.equals(DEFAULT, pro))
				continue;

			if (Objects.equals("$node", pro)) {
				lst.add(CfgContextEnvironmentAware.nodeName());
				continue;
			}

			if (pro.startsWith("$")) {
				try {
					CfgProfileResolver resolver = (CfgProfileResolver) Class.forName(pro.substring(1)).newInstance();
					lst.add(resolver.profileCode(CfgContextEnvironmentAware.environment));
				} catch (Throwable ex) {
					CfgHelper.logger.error("Error to resolve profile code of {},message is:{}", pro, ex.getMessage());
				}
				continue;
			}

			lst.add(pro);
		}

		profiles = lst.toArray(new String[lst.size()]);

		for (String prof : profiles) {
			if (this.getProertySource(prof) == null)
				addProertySourceFirst(prof, new HashMap<String, Object>());
		}
	}

	static String sourceName(String profile) {
		return SLOT_PROPERTY_SOURCE.replace("$profile$", profile);
	}

	String localCacheFileName() {
		String fileName = System.getProperty("java.io.tmpdir");

		if (!fileName.endsWith(File.separator))
			fileName += File.separator;

		fileName += DigestUtils.md5DigestAsHex(
				CfgContextEnvironmentAware.clientUniqueName(environment).getBytes(StandardCharsets.UTF_8))
				+ ".easyconfig.dat";

		return fileName;
	}

	ProfilesHelper addProertySourceFirst(String profile, Map<String, Object> properties) {
		if (properties == null)
			properties = new HashMap<String, Object>();

		String sourceName = sourceName(profile);

		environment.getPropertySources().addFirst(new MapPropertySource(sourceName, properties));

		return this;
	}

	MapPropertySource getProertySource(String profile) {
		String sourceName = sourceName(profile);

		return (MapPropertySource) environment.getPropertySources().get(sourceName);
	}

	ProfilesHelper replaceProertySource(String profile, Map<String, Object> properties) {
		if (properties == null)
			properties = new ConcurrentHashMap<String, Object>();

		String sourceName = sourceName(profile);

		environment.getPropertySources().replace(sourceName, new MapPropertySource(sourceName, properties));

		return this;
	}

	List<ConfigItemCore> updateEnvProperties(ConfigItemCore... updated) {

		List<ConfigItemCore> lst = Arrays.asList(updated);

		return updateEnvProperties(lst);
	}

	boolean inProfiles(String prof) {
		for (String p : this.profiles) {
			if (Objects.equals(p, prof))
				return true;
		}
		return false;
	}

	List<ConfigItemCore> updateEnvProperties(List<ConfigItemCore> updated) {
		if (updated.size() == 0)
			return updated;

		HashMap<String, List<ConfigItemCore>> all = new HashMap<String, List<ConfigItemCore>>();
		List<ConfigItemCore> realUpdated = new LinkedList<ConfigItemCore>();

		for (ConfigItemCore ci : updated) {
			all.computeIfAbsent(ci.item_profile, (prof) -> new LinkedList<ConfigItemCore>()).add(ci);
		}

		for (Entry<String, List<ConfigItemCore>> entry : all.entrySet()) {
			if (!this.inProfiles(entry.getKey())) {
				CfgHelper.logger.info(
						"received the config of profile {} which is not used in current application node.",
						entry.getKey());
				continue;
			}

			Map<String, Object> source = this.getProertySource(entry.getKey()).getSource();
			Map<String, Object> newMap = new ConcurrentHashMap<>(source.size());
			newMap.putAll(source);

			entry.getValue().forEach(item -> {
				try {
					if (item.deleted()) {
						if (source.containsKey(item.item_code)) {
							realUpdated.add(item);
							newMap.remove(item.item_code);
						}
					} else {
						String val = item.encrypted() ? CfgHelper.decrypt(item.item_value) : item.item_value;

						if (!Objects.equals(val, source.get(item.item_code))) {
							realUpdated.add(item);
							newMap.put(item.item_code, val);
						}
					}
				} catch (Throwable ex) {
					CfgHelper.logger.error("Error occured while invoking updateEnvProperties with {}", item);
				}
			});

			this.replaceProertySource(entry.getKey(), newMap);
		}

		return realUpdated;
	}

	Set<String> replaceEnvProperties(ConfigItemCore... updated) {

		List<ConfigItemCore> lst = Arrays.asList(updated);

		return replaceEnvProperties(lst);
	}

	Set<String> replaceEnvProperties(List<ConfigItemCore> updated) {
		HashMap<String, List<ConfigItemCore>> all = new HashMap<String, List<ConfigItemCore>>();
		HashSet<String> realUpdated = new HashSet<String>();

		for (ConfigItemCore ci : updated) {
			all.computeIfAbsent(ci.item_profile, (prof) -> new LinkedList<ConfigItemCore>()).add(ci);
		}

		for (String profile : this.profiles) {
			Map<String, Object> source = this.getProertySource(profile).getSource();
			Map<String, Object> newMap = new ConcurrentHashMap<>(source.size());

			List<ConfigItemCore> list = all.get(profile);
			if (list == null)
				list = new LinkedList<ConfigItemCore>();

			list.forEach(item -> {
				try {
					if (item.deleted()) {
//						if (source.containsKey(item.item_code))
//							realUpdated.add(item.key());
						return;
					}

					String item_value = item.encrypted() ? CfgHelper.decrypt(item.item_value) : item.item_value;
					newMap.put(item.item_code, item_value);

					if (!Objects.equals(item_value, source.get(item.item_code)))
						realUpdated.add(item.key());

				} catch (Throwable ex) {
					CfgHelper.logger.error("Error occured while invoking replaceEnvProperties with {}", item);
				}
			});

			source.forEach((code, value) -> {
				if (!newMap.containsKey(code)) {
					realUpdated.add(ConfigItemCore.key(profile, code));
				}
			});

			this.replaceProertySource(profile, newMap);
		}

		return realUpdated;
	}

	ProfilesHelper loadLocalCache() {
		ByteArrayInputStream bais = null;
		ObjectInputStream ois = null;
		try {
			String localCacheFileName = localCacheFileName();

			bais = new ByteArrayInputStream(Files.readAllBytes(Paths.get(localCacheFileName)));
			ois = new ObjectInputStream(bais);

			@SuppressWarnings("unchecked")
			Map<String, Object> map = (Map<String, Object>) ois.readObject();

			if (!"easyconfig".equals(map.get("source"))) {
				CfgHelper.logger.info("Load local cached config of {}", localCacheFileName);
				return this;
			}

			@SuppressWarnings("unchecked")
			Map<String, Map<String, Object>> content = (Map<String, Map<String, Object>>) map.get("content");
			for (String prof : this.profiles) {
				Map<String, Object> source = (Map<String, Object>) content.get(prof);
				this.replaceProertySource(prof, source);
			}

		} catch (Throwable e) {
			CfgHelper.logger.info("Fail to load local cached config. {}", e.getMessage());
		} finally {
			try {
				if (ois != null)
					ois.close();
				if (bais != null)
					bais.close();
			} catch (Throwable e) {
			}
		}
		return this;
	}

	ProfilesHelper storeLocalCache() {
		ByteArrayOutputStream baos = null;
		ObjectOutputStream oos = null;
		try {
			HashMap<String, Object> content = new HashMap<String, Object>();

			for (String prof : this.profiles) {
				content.put(prof, this.getProertySource(prof).getSource());
			}

			HashMap<String, Object> map = new HashMap<String, Object>();
			map.put("content", content);
			map.put("source", "easyconfig");
			map.put("version", 1);

			String fileName = localCacheFileName();

			baos = new ByteArrayOutputStream(1024);
			oos = new ObjectOutputStream(baos);
			oos.writeObject(map);
			Files.write(Paths.get(fileName), baos.toByteArray());

			CfgHelper.logger.debug("The config is cached into local file. {}", fileName);
		} catch (Throwable e) {
			CfgHelper.logger.info("An error occurred when to store config locally. {}", e.getMessage());
		} finally {
			try {
				if (oos != null)
					oos.close();
				if (baos != null)
					baos.close();
			} catch (Throwable e) {
			}
		}
		return this;
	}

	ProfilesHelper deleteLocalCache() {
		String fileName = "";
		try {
			fileName = localCacheFileName();
			Files.deleteIfExists(Paths.get(fileName));
		} catch (Throwable e) {
			CfgHelper.logger.info("An error occurred when to delete local cache file of {}. message : {}", fileName,
					e.getMessage());
		}
		return this;
	}

}
